接下來要進行一系列的測試,會涵蓋 Database 到 Activity/Fragment 等部分,下面就先開始進行 DB 的單元測試。
首先需要 import 幾個比較重要的 library ,例如 AndroidX Test 、 MockK、Google Truth 、Erpresso 、Robolectric,同時也要為 Room 、 Dagger 加入他們專屬的 Testing compiler library ,這裏就不一一細講了,具體 import 的 library 可以看看 Gist 。
在寫測試之前,需要知道在 Android 裡的測試可以大致分成兩個 folder:
以今天的主題為例,就會選擇在 androidTest 裡完成。
...以上是大部分教學所提到的內容,但我個人習慣在寫測試前先調整一下存放 Test 的位置。
由於現在在寫測試時常常發生既會使用 Android API 又要用 JUnit 寫一些單元測試,又或者一些 Test Util 可能在上面兩類測試都會用到,遇到這些情況把 class 放在哪裡都不太好,所以我另外會再開一個 share folder ,用來放這些 class 。
首先 build.gradle 內宣告創建一個跨兩個測試的資料夾 sharedTest :
android {
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test {
java.srcDir sharedTestDir
}
androidTest {
java.srcDir sharedTestDir
}
}
}
接著因為我的專案名稱是 com.ininmm.todpapp
,所以建立以下資料夾:src/sharedTest/java/com/ininmm/todoapp
回到剛剛話題, Database 的測試會用到 Android API 及一些 JUnit 的東西,所以待會的測試就可以寫在這裡了。
今天要對 Room 的 Dao 進行測試,前面提到了 Dao 有使用 Android API ,所以讓我們先在 sharedTest 下建立一個 Test Class TasksDaoTest
:
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {
}
@Dao
interface TasksDao {
@Query("SELECT * FROM tasks")
suspend fun getTasks(): List<Task>
@Query("SELECT * FROM tasks WHERE entryid = :taskId")
suspend fun getTaskById(taskId: String): Task?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task): Long
@Update
suspend fun updateTask(task: Task): Int
@Query("UPDATE Tasks SET completed = :completed WHERE entryid = :taskId")
suspend fun updateComplete(taskId: String, completed: Boolean)
@Query("DELETE FROM Tasks WHERE entryid = :taskId")
suspend fun deleteTaskById(taskId: String): Int
@Query("DELETE FROM Tasks")
suspend fun deleteTasks()
@Query("DELETE FROM Tasks WHERE completed = 1")
suspend fun deleteCompletedTasks(): Int
}
因為我們在 Room 中有使用其 coroutines 的部分,所以標上 ExperimentalCoroutinesApi
表明 coroutines 的某些 API 未來可能會變動。
使用 RunWith
告知 JUnit 接下來要執行的測試要使用那個 class 執行,而 AndroidJUnit4
正式 Android 用來在 Android 環境中執行 JUnit 的 runner 。
標上 SmallTest
則是 Android 告訴測試這是一個輕量、執行時間短的 Test ,有以下幾種 annotation 可供選擇:
接著設置在每次開始測試前,先取得 Room 的 DataBase ,這邊可以使用 in-memory database ,可以在不影響 app 裡的資料下進行 DB 的測試,當然在完成測試後,也需要把 Database 關閉。
另外由於 Architecture Components 在執行操作時會自動切換到他自己的 background executor 執行,需要再加上 InstantTaskExecutorRule
,這樣執行測試時可以把 Architecture Components 的 thread 切換成一個同步的 executor 。
完成的程式碼如下:
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {
private lateinit var roomDatabase: ToDoRoomDatabase
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setup() {
roomDatabase = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoRoomDatabase::class.java
).allowMainThreadQueries().build()
}
@After
fun dropdown() {
roomDatabase.close()
}
}
再來看看寫測試時的方法命名,通常測試方法的名稱有一定的規範,共同目標旨在說明這個測試要測什麼,預期結果是什麼。
假設要測試 TasksDao.getTaskById()
這個方法,可以建立一個 function 叫做 when_insert_task_then_get_by_id_success()
,表示這個測試的前提是先 insertTask()
,並且測試 getTaskById()
驗證結果成功。
關於命名的方式有許多流派,更詳細的資訊可以查看這裡,可以自己選擇一個喜歡的命名方式,反正只要能夠描述清楚測試的目標及結果即可。
最後具體的寫法,我們先寫一個基於 BBD 的測試,基本上只需要遵守 Given-When-Then 的原則就好。
接下來看看完成的測試內容:
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {
private lateinit var roomDatabase: ToDoRoomDatabase
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setup() {
roomDatabase = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoRoomDatabase::class.java
).allowMainThreadQueries().build()
}
@Test
fun when_insert_task_then_get_by_id_success() = runBlockingTest {
// Given:進行初始化,先往 Database 塞入一個 Task
val task = Task("title", "description")
roomDatabase.tasksDao().insertTask(task)
// When:執行 getTaskById() 後獲得結果
val loaded = roomDatabase.tasksDao().getTaskById(task.id)
// Then:對結果進行驗證
assertThat<Task>(loaded as Task, notNullValue())
assertThat(loaded.id, `is`(task.id))
assertThat(loaded.title, `is`(task.title))
assertThat(loaded.description, `is`(task.description))
assertThat(loaded.isCompleted, `is`(task.isCompleted))
}
@After
fun dropdown() {
roomDatabase.close()
}
}
篇幅有限無法在這裡貼上全部的測試程式,有興趣可以看看這邊,但我想今天已經完成第一個簡單的測試了。